﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Channels;

namespace N3P.WCF.Proxyless
{
    /// <summary>
    /// Global options for all <see cref="WcfClient{T}"/>
    /// </summary>
    public class WcfClient
    {
        /// <summary>
        /// Gets whether or not all auto-post back to the calling synchronization context are skipped
        /// </summary>
        internal static bool IsGlobalManualCallbackSynchronization { get; private set; }

        /// <summary>
        /// Sets all clients to use manual callback synchronization
        /// </summary>
        public static void SetManualCallbackSynchronizationGlobally()
        {
            IsGlobalManualCallbackSynchronization = true;
        }
    }

    /// <summary>
    /// Proxy class for handling calls to a WCF service
    /// </summary>
    /// <typeparam name="T">The service contract with synchronous methods</typeparam>
    public class WcfClient<T> : WcfClient, IDisposable
    {
		#region Fields (1) 

        /// <summary>
        /// The actual WCF client
        /// </summary>
        private readonly object _wcfClient;

		#endregion Fields 

		#region Constructors (5) 

        /// <summary>
        /// Initializes a new instance of the <see cref="T:N3P.WCF.Proxyless.WcfClient`1" /> class.
        /// </summary>
        /// <param name="endpointConfigurationName">The name of the endpoint in the application configuration file</param>
        /// <param name="remoteAddress">The address of the service</param>
        public WcfClient(string endpointConfigurationName, string remoteAddress)
        {
            _wcfClient = AsyncClientCreator<T>.GetClient(endpointConfigurationName, remoteAddress);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:N3P.WCF.Proxyless.WcfClient`1" /> class using the specified target address and endpoint information
        /// </summary>
        /// <param name="endpointConfigurationName">The name of the endpoint in the application configuration file.</param>
        /// <param name="remoteAddress">The address of the service.</param>
        public WcfClient(string endpointConfigurationName, EndpointAddress remoteAddress)
        {
            _wcfClient = AsyncClientCreator<T>.GetClient(endpointConfigurationName, remoteAddress);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:N3P.WCF.Proxyless.WcfClient`1" /> class using the specified binding and target address
        /// </summary>
        /// <param name="binding">The binding with which to make calls to the service.</param>
        /// <param name="remoteAddress">The address of the service endpoint.</param>
        public WcfClient(Binding binding, EndpointAddress remoteAddress)
        {
            _wcfClient = AsyncClientCreator<T>.GetClient(binding, remoteAddress);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:N3P.WCF.Proxyless.WcfClient`1" /> class using the configuration information specified in the application configuration file by <paramref cref="endpointConfigurationName" />.
        /// </summary>
        /// <param name="endpointConfigurationName"></param>
        public WcfClient(string endpointConfigurationName)
        {
            _wcfClient = AsyncClientCreator<T>.GetClient(endpointConfigurationName);
        }

        /// <summary>
        /// Initializes a new instance of the WcfCaller class using the default target endpoint from the application configuration file.
        /// </summary>
        public WcfClient()
        {
            _wcfClient = AsyncClientCreator<T>.GetClient();
        }

		#endregion Constructors 

		#region Methods (7) 

        internal static bool IsManualCallbackSynchronization { get; private set; }

        /// <summary>
        /// Sets this particular client to use manual callback synchronization
        /// </summary>
        public static void SetManualCallbackSynchronization()
        {
            IsManualCallbackSynchronization = true;
        }

		// Public Methods (5) 

        /// <summary>
        /// Calls a method without a return on the service
        /// </summary>
        /// <param name="callExpression">The expression that will be used to call the service method</param>
        /// <param name="continue">The handler for the result</param>
        public void Call(Expression<Action<T>> callExpression, Action<Exception> @continue)
        {
            Call(callExpression, @continue, false);
        }

        /// <summary>
        /// Calls a method without a return on the service
        /// </summary>
        /// <param name="callExpression">The expression that will be used to call the service method</param>
        /// <param name="continue">The handler for the result</param>
        /// <param name="skipPost">Whether or not to skip posting back to the calling synchronization context if one is used</param>
        public void Call(Expression<Action<T>> callExpression, Action<Exception> @continue, bool skipPost)
        {
            var skip = skipPost || IsManualCallbackSynchronization || IsGlobalManualCallbackSynchronization;
            var methodCall = (MethodCallExpression) callExpression.Body;
            var exprArgs = methodCall.Arguments;
            var args = new List<object>();

            foreach(var arg in exprArgs)
            {
                try
                {
                    var value = Expression.Lambda(arg).Compile().DynamicInvoke();
                    args.Add(value);
                }
                catch (Exception ex)
                {
                    var inner = Unwrap(ex);
                    throw new ArgumentException("Evaluation of " + arg + " threw an exception", inner);
                }
            }

            var method = methodCall.Method;
            var beginMethod = AsyncClientCreator<T>.BeginMethodMap[method];
            var endMethod = AsyncClientCreator<T>.EndMethodMap[method];

            var performContinue =
                new AsyncCallback(x =>
                {
                    try
                    {
                        endMethod.Invoke(_wcfClient, new object[] {x});
                        ProxyCallBackToUIThread(null, @continue, skip);
                    }
                    catch (TargetInvocationException ex)
                    {
                        var current = Unwrap(ex);
                        ProxyCallBackToUIThread(current, @continue, skip);
                    }
                    catch (Exception ex)
                    {
                        ProxyCallBackToUIThread(ex, @continue, skip);
                    }
                });
            args.Add(performContinue);
            args.Add(null);

            beginMethod.Invoke(_wcfClient, args.ToArray());
        }

        /// <summary>
        /// Calls a method with a return on the service
        /// </summary>
        /// <typeparam name="TResult">The type of result the call should have</typeparam>
        /// <param name="callExpression">The expression that will be used to call the service method</param>
        /// <param name="continue">The handler for the result</param>
        public void Call<TResult>(Expression<Func<T, TResult>> callExpression, Action<TResult, Exception> @continue)
        {
            Call(callExpression, @continue, false);
        }

        /// <summary>
        /// Calls a method with a return on the service
        /// </summary>
        /// <typeparam name="TResult">The type of result the call should have</typeparam>
        /// <param name="callExpression">The expression that will be used to call the service method</param>
        /// <param name="continue">The handler for the result</param>
        /// <param name="skipPost">Whether or not to skip posting back to the calling synchronization context if one is used</param>
        public void Call<TResult>(Expression<Func<T, TResult>> callExpression, Action<TResult, Exception> @continue, bool skipPost)
        {
            var skip = skipPost || IsManualCallbackSynchronization || IsGlobalManualCallbackSynchronization;
            var methodCall = (MethodCallExpression) callExpression.Body;
            var exprArgs = methodCall.Arguments;

            var args = exprArgs.Select(arg => Expression.Lambda(arg).Compile().DynamicInvoke()).ToList();

            var method = methodCall.Method;
            var beginMethod = AsyncClientCreator<T>.BeginMethodMap[method];
            var endMethod = AsyncClientCreator<T>.EndMethodMap[method];

            var performContinue =
                new AsyncCallback(x =>
                {
                    try
                    {
                        var result = (TResult) endMethod.Invoke(_wcfClient, new object[] {x});
                        ProxyCallBackToUIThread(result, null, @continue, skip);
                    }
                    catch (TargetInvocationException ex)
                    {
                        var current = Unwrap(ex);
                        ProxyCallBackToUIThread(default(TResult), current, @continue, skip);
                    }
                    catch (Exception ex)
                    {
                        ProxyCallBackToUIThread(default(TResult), ex, @continue, skip);
                    }
                });

            args.Add(performContinue);
            args.Add(null);

            beginMethod.Invoke(_wcfClient, args.ToArray());
        }

        /// <summary>
        /// Perform the construction of the underlying client in a fire and forget style
        /// </summary>
        public static void CreateClientAsync()
        {
            CreateClientAsync(SharedResources.DefaultAction);
        }

        /// <summary>
        /// Perform the construction of the underlying client if necessary and perform the specified action when done
        /// </summary>
        /// <param name="callback">The action to perform when the client has been generated</param>
        public static void CreateClientAsync(Action callback)
        {
            var worker = new BackgroundWorker();
            worker.DoWork += (sender, args) => AsyncClientCreator<T>.EnsureClientDefinition();
            worker.RunWorkerCompleted += (sender, args) => (callback ?? SharedResources.DefaultAction)();
            worker.RunWorkerAsync();
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            AsyncClientCreator<T>.CloseMethod(_wcfClient);
        }
		// Private Methods (2) 

        /// <summary>
        /// Proxies a continuation back to the UI thread
        /// </summary>
        /// <param name="exception">The exception that occurred during the operation</param>
        /// <param name="continue">The action to perform on the UI thread with the operation's result</param>
        /// <param name="skipPost">Whether or not to skip posting back to the calling synchronization context if one is used</param>
        private static void ProxyCallBackToUIThread(Exception exception, Action<Exception> @continue, bool skipPost)
        {
            SharedResources.InvokeOnUIThread(() => @continue(exception), skipPost);
        }

        /// <summary>
        /// Proxies a continuation back to the UI thread
        /// </summary>
        /// <typeparam name="TResult">The type of the result of the operation</typeparam>
        /// <param name="result">The result of the operation</param>
        /// <param name="exception">The exception that occurred during the operation</param>
        /// <param name="continue">The action to perform on the UI thread with the operation's result</param>
        /// <param name="skipPost">Whether or not to skip posting back to the calling synchronization context if one is used</param>
        private static void ProxyCallBackToUIThread<TResult>(TResult result, Exception exception, Action<TResult, Exception> @continue, bool skipPost)
        {
            SharedResources.InvokeOnUIThread(() => @continue(result, exception), skipPost);
        }

        private static Exception Unwrap(Exception ex)
        {
            if(ex is TargetInvocationException && ex.InnerException != null)
            {
                return Unwrap(ex.InnerException);
            }

            return ex;
        }

		#endregion Methods 
    }
}
